package com.chenyun.satoken.starter.util;

import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.annotation.*;
import cn.dev33.satoken.basic.SaBasicUtil;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.fun.strategy.SaCheckElementAnnotationFunction;
import cn.dev33.satoken.jwt.exception.SaJwtException;
import cn.dev33.satoken.same.SaSameUtil;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.*;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.fastjson2.JSON;
import com.chenyun.common.constants.SaTokenConstant;
import com.chenyun.common.constants.SysConstants;
import com.chenyun.common.constants.UserConstants;
import com.chenyun.common.context.ServletContext;
import com.chenyun.common.context.SpringContext;
import com.chenyun.common.context.UserContextHolder;
import com.chenyun.common.enums.LoginUserTypeEnum;
import com.chenyun.common.event.TokenResolveEvent;
import com.chenyun.common.exceptions.BusinessException;
import com.chenyun.common.utils.StringUtils;
import com.chenyun.common.vo.SessionUser;
import com.chenyun.satoken.starter.interceptor.SaMultipleAccountInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * sa登录帮助类
 *
 * @author: Lonni
 * @date: 2022/8/18 0018 11:46
 */

public class SaUtil {
    private static final Logger logger = LoggerFactory.getLogger(SaUtil.class);

    //-------------------------自定义校验逻辑开始------------------------------


    /**
     * 重写satoken的注解校验
     * 支持多账户模式
     */
    public static SaCheckElementAnnotationFunction checkMultipleAccountElementAnnotation = (element) -> {

        logger.info("进入注解校验逻辑-多账户版");
        String loginType = getLoginType();
        logger.info("当前的loginType类型:{}", loginType);


        SaStrategy instance = SaStrategy.me;
        SaCheckLogin checkLogin = (SaCheckLogin) instance.getAnnotation.apply(element, SaCheckLogin.class);
        SaCheckRole checkRole = (SaCheckRole) instance.getAnnotation.apply(element, SaCheckRole.class);
        SaCheckPermission checkPermission = (SaCheckPermission) instance.getAnnotation.apply(element, SaCheckPermission.class);
        SaCheckSafe checkSafe = (SaCheckSafe) instance.getAnnotation.apply(element, SaCheckSafe.class);
        SaCheckDisable checkDisable = (SaCheckDisable) instance.getAnnotation.apply(element, SaCheckDisable.class);
        SaCheckBasic checkBasic = (SaCheckBasic) instance.getAnnotation.apply(element, SaCheckBasic.class);
        if (ObjectUtil.isAllEmpty(checkLogin, checkRole, checkPermission, checkSafe, checkDisable, checkBasic)) {
            return;
        }


        //将loginType装载到上下文中

        if (checkLogin != null) {
            SaManager.getStpLogic(loginType, false).checkByAnnotation(checkLogin);
        }

        if (checkRole != null) {
            SaManager.getStpLogic(loginType, false).checkByAnnotation(checkRole);
        }

        if (checkPermission != null) {
            SaManager.getStpLogic(loginType, false).checkByAnnotation(checkPermission);
        }

        if (checkSafe != null) {
            SaManager.getStpLogic(loginType, false).checkByAnnotation(checkSafe);
        }

        if (checkDisable != null) {
            SaManager.getStpLogic(loginType, false).checkByAnnotation(checkDisable);
        }

        if (checkBasic != null) {
            SaBasicUtil.check(loginType, checkBasic.account());
        }

    };

    //-------------------------自定义校验逻辑结束------------------------------


    //-------------------------登录逻辑开始------------------------------

    /**
     * 装载 saLoginModel参数
     *
     * @param loginUser
     * @return
     */
    static SaLoginModel convertSaLoginModel(SessionUser loginUser) {
        //为了兼容从token中获取,将sessionUser信息写入到token中
        Map<String, Object> stringMap = new HashMap<>(7);
        stringMap.put("userId", loginUser.getUserId());
        stringMap.put("userName", loginUser.getUserName());
        stringMap.put("shopId", loginUser.getShopId());
        stringMap.put("shopName", loginUser.getShopName());
        stringMap.put("unionId", loginUser.getUnionId());
        stringMap.put("openId", loginUser.getOpenId());
        stringMap.put("deptId", loginUser.getDeptId());
        stringMap.put("device", loginUser.getDeviceType().getDevice());
        stringMap.put("userType", loginUser.getUserType().getType());
        stringMap.put("clientId", loginUser.getClientId());

        SaLoginModel saLoginModel = SaLoginConfig.create();
        saLoginModel.setDevice(loginUser.getDeviceType().getDevice());
        saLoginModel.setExtraData(stringMap);
        /**
         *  写此代码的原因是单体项目中多账户登录情况
         *   sa-token中 account-session和token-session过期时间都是使用全局的timeout时间,
         *   在单体项目下app或者小程序登录需要设置为更长的时间过期(7天),但是pc的话一般2小时
         *   所以这里增加特殊处理 ;
         *   增加特殊处理后需要手动修改redis中token-session的过期时间和account-session时间一致
         *   请在登录成功后调用  类方法 setTokenSessionTimeOut() 设置过期时间
         */
        //如果是小程序或则app ,指定7天后过期 凌晨1点过期;也就是第8天的凌晨1点
        //其他设置为默认
//        if (loginUser.getDeviceType() == LoginDeviceTypeEnum.MP
//                || loginUser.getDeviceType() == LoginDeviceTypeEnum.APP) {
//            LocalDateTime now = LocalDateTime.now();
//            //将当前日期增加8天
//            LocalDateTime localDate = now.plusDays(8L);
//            //获取到年月日
//            int year = localDate.getYear();
//            int month = localDate.getMonthValue();
//            int day = localDate.getDayOfMonth();
//            //设置8天后的凌晨1点过期
//            LocalDateTime end = LocalDateTime.of(year, month, day, 1, 0, 0);
//            //拿到秒数
//            Duration between = Duration.between(now, end);
//            //设置超时时间
//            saLoginModel.setTimeout(between.getSeconds());
//        }
        return saLoginModel;
    }

    /**
     * 根据用户类型拿到stp需要的loginType 值
     *
     * @param typeEnum 登录类型
     * @return loginType
     */
    public static String getLoginTypeByUserType(LoginUserTypeEnum typeEnum) {
        if (typeEnum == LoginUserTypeEnum.PLAT_FORM_ADMIN) {
            return StpUtil.TYPE;
        }
        if (typeEnum == LoginUserTypeEnum.BUS_ADMIN) {
            return SaTokenConstant.SHOP_LOGIN_TYPE;
        }
        if (typeEnum == LoginUserTypeEnum.MEMBER) {
            return SaTokenConstant.MEMBER_LOGIN_TYPE;
        }
        return null;
    }

    public static LoginUserTypeEnum getUserTypeByLoginType(String loginType) {
        switch (loginType) {
            case StpUtil.TYPE:
                return LoginUserTypeEnum.PLAT_FORM_ADMIN;
            case SaTokenConstant.MEMBER_LOGIN_TYPE:
                return LoginUserTypeEnum.MEMBER;
            case SaTokenConstant.SHOP_LOGIN_TYPE:
                return LoginUserTypeEnum.BUS_ADMIN;
            default:
                return null;


        }


    }


    /**
     * 登录系统 基于 设备类型
     * 针对相同用户体系不同设备
     *
     * @param loginUser 登录用户信息
     */
    public static void loginByDevice(SessionUser loginUser) {
        String loginTypeByUserType = getLoginTypeByUserType(loginUser.getUserType());
        SaLoginModel model = convertSaLoginModel(loginUser);
        SaManager.getStpLogic(loginTypeByUserType, false).login(loginUser.getUserId(), model);
        //登录成功 将操作类加入到上下文
        SatokenContext.putLoginType(loginTypeByUserType);
        SatokenContext.instance.setStpLogic(loginTypeByUserType, SaManager.getStpLogic(loginTypeByUserType, false));
        // 拿到token信息
        SaTokenInfo tokenInfo = SatokenContext.instance.getStpLogic().getTokenInfo();
        setLoginUser(loginUser, tokenInfo);
    }

    /**
     * 设置用户数据
     */
    static void setLoginUser(SessionUser loginUser, SaTokenInfo tokenInfo) {
        String loginTypeByUserType = tokenInfo.getLoginType();
        String userKey = UserConstants.LOGIN_USER_KEY;
        String roleKey = UserConstants.USER_ROLE_CACHE_KEY;
        String permissionKey = UserConstants.USER_PERMISSION_CACHE_KEY;
        SaSession session = SatokenContext.instance.getStpLogic(loginTypeByUserType)
                .getTokenSession();
        session.set(userKey, loginUser);
        //缓存用户角色信息和权限信息
        //角色
        if (CollectionUtil.isNotEmpty(loginUser.getRolePermission())) {
            session.set(roleKey, loginUser.getRolePermission());
        }
        //菜单权限
        if (CollectionUtil.isNotEmpty(loginUser.getMenuPermission())) {
            session.set(permissionKey, loginUser.getMenuPermission());
        }
        //将用户token和session保存到redis中,方便下一个微服务获取;
        //过期时间 token的过期时间加上10秒
        RedisTemplate redisTemplate = SpringUtil.getBean(RedisTemplate.class);
        //去掉权限信息
//        SessionUser cloneUser = ObjectUtil.clone(loginUser);
//        cloneUser.setMenuPermission(null);
//        cloneUser.setRolePermission(null);
        loginUser.setPassword("");
        redisTemplate.opsForValue().set(SaTokenConstant.TOKEN_SESSION_KEY + tokenInfo.getTokenValue(),
                loginUser, tokenInfo.getTokenTimeout() + 10, TimeUnit.SECONDS);

    }

    /**
     * 从缓存中获取当前登录用户
     */
    public static SessionUser sessionUser() {
        StpLogic stpLogic = SatokenContext.instance.getStpLogic();
        String tokenValue = stpLogic.getTokenValue();
        RedisTemplate redisTemplate = SpringUtil.getBean(RedisTemplate.class);
        SessionUser user = (SessionUser) redisTemplate.opsForValue().get(SaTokenConstant.TOKEN_SESSION_KEY + tokenValue);
        return user;
    }


    /**
     * 退出登录
     */
    public static void logout() {
        String userId = getUserId();
        SaManager.getStpLogic(SatokenContext.getLoginType(), false).logout(userId);
        RedisTemplate redisTemplate = SpringUtil.getBean(RedisTemplate.class);
        //清除session
        redisTemplate.delete(SaTokenConstant.TOKEN_SESSION_KEY);
    }


    /**
     * 校验是否登录
     * 此方法入口拦截器调用
     *
     * @see SaMultipleAccountInterceptor
     */


    /**
     * feign请求检查
     */
    public static void feignCheck() {
        String tokenName = SpringUtil.getApplicationContext().getEnvironment().getProperty("sa-token.token-name");
        // 拿到当前token
        String tokenValue = ServletContext.getRequest().getHeader(tokenName);
        //凭借前缀
        String prefix = SpringUtil.getApplicationContext().getEnvironment().getProperty("sa-token.token-prefix");
        String token = prefix + " " + tokenValue;
        if (StrUtil.isBlank(tokenValue)) {
            throw new BusinessException("未检测到token!");
        }
        //根据token拿到sessionUser
        RedisTemplate redisTemplate = SpringUtil.getBean(RedisTemplate.class);
        Object o = redisTemplate.opsForValue().get(SaTokenConstant.TOKEN_SESSION_KEY + tokenValue);
        if (o == null) {
            throw new BusinessException("未检测到token!");
        }
        SessionUser user = (SessionUser) o;
        //装载到用户上下文
        UserContextHolder.putUser(user);
        String loginType = getLoginTypeByUserType(user.getUserType());
        SatokenContext.putLoginType(loginType);
        SatokenContext.instance.setStpLogic(loginType, SaManager.getStpLogic(loginType));

    }

    /**
     * rpc校验调用
     *
     * @param idToken
     * @param tokenValue
     */
    public static void rpcCheck(String idToken, String tokenValue) {
        SaSameUtil.checkToken(idToken);
        String prefix = SpringUtil.getApplicationContext().getEnvironment().getProperty("sa-token.token-prefix");
        String token = prefix + " " + tokenValue;
        if (StrUtil.isBlank(tokenValue)) {
            return;
        }
        if (tokenValue.startsWith("Bearer ")) {
            tokenValue = tokenValue.replace("Bearer ", "");
        }
        //根据token拿到sessionUser
        if (StringUtils.isNotBlank(tokenValue)) {
            logger.info("token不为空,开始校验token");
            RedisTemplate redisTemplate = SpringUtil.getBean(RedisTemplate.class);
            Object o = redisTemplate.opsForValue().get(SaTokenConstant.TOKEN_SESSION_KEY + tokenValue);
            if (o == null) {
                throw new BusinessException("未检测到token!");
            }
            SessionUser user = (SessionUser) o;
            //装载到用户上下文
            UserContextHolder.putUser(user);
            String loginType = getLoginTypeByUserType(user.getUserType());
            SatokenContext.putLoginType(loginType);
            SatokenContext.instance.setStpLogic(loginType, SaManager.getStpLogic(loginType));
        }
    }

    public static void checkLogin() {
        SatokenContext.instance.getStpLogic().checkLogin();
    }

    /**
     * 校验登录 并赋值租户id
     *
     * @param isIniTenantId
     */
    public static void checkLogin(Boolean isIniTenantId) {
        logger.info("校验是否登录,并赋值租户id");
        checkLogin();
        if (isIniTenantId) {
            SpringContext.publishEvent(new TokenResolveEvent(SaMultipleAccountInterceptor.class, true));
        }

    }

    //-------------------------登录逻辑结束 此时SatokenContext中都能拿到StpLogic和Sassion------------------------------


    /**
     * 拿到当前会话的loginType
     *
     * @return
     */
    public static String getLoginType() {
        if (StrUtil.isBlank(SatokenContext.getLoginType())) {
            String tokenValue = StpUtil.getStpLogic().getTokenValue();
            if (StringUtils.isBlank(tokenValue)) {
                throw new SaJwtException("无效token");
            }
            // 因为重写了token生成策略 loginType+""+token
            //所以只需要截取即可
            String[] tokenSplit = tokenValue.split("\\" + SaTokenConstant.TOKEN_LOGIN_TYPE_SPLIT_PREFIX);
            if (tokenSplit.length < 2) {
                throw new SaJwtException("无效token");
            }
            SatokenContext.putLoginType(tokenSplit[0]);
        }
        return SatokenContext.getLoginType();
    }


    /**
     * 获取用户
     */
    public static SessionUser getLoginUser() {
        try {
            SessionUser user = UserContextHolder.getUser();
            if (user == null) {

                String userKey = UserConstants.LOGIN_USER_KEY;
                SaSession tokenSession = SatokenContext.instance.getSession();
                SessionUser sessionUser = JSON.parseObject((tokenSession.get(userKey)).toString(), SessionUser.class);
                //装载到上下文
                UserContextHolder.putUser(sessionUser);
                return sessionUser;
            }

            return user;
        } catch (Exception ex) {
            logger.error("getLoginUser fail ", ex);
            return null;
        }
    }

    /**
     * 获取当前用户
     *
     * @return
     */
    public static Optional<SessionUser> currentUser() {
        SessionUser loginUser = getLoginUser();
        return Optional.ofNullable(loginUser);
    }

    /**
     * 获取系统登录用户id
     *
     * @return
     */
    public static String getUserId() {
        String loginType = SatokenContext.getLoginType();
        return SatokenContext.instance.getStpLogic(loginType).getLoginId().toString();

    }


    /**
     * 获取用户账户
     */
    public static String getUsername() {
        return getLoginUser().getUserName();
    }

    /**
     * 获取用户类型
     */
    public static LoginUserTypeEnum getUserType() {
        LoginUserTypeEnum userType = getLoginUser().getUserType();
        return userType;
    }


    /**
     * 是否是超级管理员
     *
     * @return
     */
    public static Boolean isAdmin() {
        SessionUser user = UserContextHolder.getUser();
        if (user == null) {
            user = getLoginUser();
        }
        boolean contains = user.getRolePermission().contains(SysConstants.PLATFORM_ADMIN_ROLE);
        return contains;
    }


    /**
     * 是否是运营|后台用户登录
     *
     * @return
     */
    public static boolean isPlatformUser() {
        return getUserType().equals(LoginUserTypeEnum.PLAT_FORM_ADMIN);
    }

    /**
     * 是否是会员|买家
     *
     * @return
     */
    public static boolean isMemberUser() {
        return getUserType().equals(LoginUserTypeEnum.MEMBER);
    }


    /**
     * 设置token-session时间和account-session一致
     *
     * @param saTokenInfo
     */
    public static void setTokenSessionTimeOut(SaTokenInfo saTokenInfo) {
        StpLogic stpLogic = SaManager.getStpLogic(saTokenInfo.getLoginType(), false);
        SaTokenDao saTokenDao = stpLogic.getSaTokenDao();
        String tokenKey = stpLogic.splicingKeyTokenValue(saTokenInfo.getTokenValue());
        String tokenSessionKey = stpLogic.splicingKeyTokenSession(saTokenInfo.getTokenValue());
        long timeout = saTokenDao.getTimeout(tokenKey);
        logger.info("token过期时间为:{}", timeout);
        //设置token-session过期时间
        saTokenDao.updateTimeout(tokenSessionKey, timeout + 10);
        logger.info("设置后的token-session过期时间为:{}", saTokenDao.getTimeout(tokenSessionKey));

    }

}
